package tests;
/* --- THE OCEAN WORLD -----------------------------------------------
Fish swim across the screen from right to left. There may be one fish,
or a whole school of fish, or none at a time. A hungry shark swims in
place at the left side, moving only up and down, controlled by the
"up" and "down" keys. It tries to catch and eat the fish. 
It gets bigger with each fish it eats, it keeps getting hungrier
and hungrier as the time goes on between the catches. 
It dies of starvation, if not fed in time.
--------------------------------------------------------------------*/

import colors.*;
import idraw.*;
import geometry.*;
import java.util.Random;
import tester.*;

/*
+----------+
| SharkImp |
+----------+
| Posn loc |
| int life |
+----------+

*/

/**
 * Copyright 2007, 2008 Viera K. Proulx
 * This program is distributed under the terms of the 
 * GNU Lesser General Public License (LGPL)
 */


// to represent a shark in an ocean world
class SharkImp {
  Posn loc;
  int life;
  int MaxLIFESPAN = 220;

  SharkImp(Posn loc, int life) {
    this.loc = loc;
    this.life = life;
  }
  
  // produce a new shark from this shark moved in response to the key press
  void onKeyEvent(String ke){
    if (ke.equals("up"))
      this.loc = new Posn(this.loc.x, this.loc.y - 3);
    else if (ke.equals("down"))
      this.loc = new Posn(this.loc.x, this.loc.y + 3);
  }
  
  // draw this shark on the given Canvas
  void draw(Canvas c){
    c.drawDisk(this.loc, this.life / 10 + 2, new Black());
    c.drawDisk(new Posn(this.loc.x + 2, this.loc.y), 1, new Red());
    c.drawLine(this.loc, 
               new Posn(this.loc.x + 25, this.loc.y), 
               new Black());
  }
  
  // produce a shark hungrier by a minute than this one
  void onTick(){
    if (this.life > 0)
      this.life = this.life - 1;
  }
    
  // produce a shark after this one ate one fish
  // for now every fish tastes the same
  // later, fish nutrition value may be determined by the size or the color 
  void getFatter(){
    this.life = Math.min(this.life + 30, this.MaxLIFESPAN);
  }
  
  // is this shark dead?
  boolean isDead(){
    return this.life <= 0;
  }
  
}

/*
+-----------------+
| FishImp         |
+-----------------+
| Posn loc        |
| boolean escaped |
+-----------------+

*/

// to represent a fish in an ocean world
class FishImp {
  Posn loc;
  boolean hasEscaped;
  int speed;

  FishImp(Posn loc, boolean hasEscaped, int speed) {
    this.loc = loc;
    this.hasEscaped = hasEscaped;
    this.speed = speed;
  }
  
  // produce from this fish the one that swam for a minute more
  void onTick(){
    if (hasEscaped){
      this.loc = new Posn(200, 200);
      this.hasEscaped = false;
    }
    else if (this.loc.x < 0)
      this.hasEscaped = true;
    // keep the fishy inbounds - refactor this!!!!
    else if (this.loc.y < 0)
      this.loc = new Posn(this.loc.x, - this.loc.y);
    else if (this.loc.y > 200)
      this.loc = new Posn(this.loc.x, this.loc.y % 200);
    else
      this.loc = new Posn(this.loc.x - this.speed, this.loc.y + 4);
  }
  
  // has this fish escaped?
  boolean escaped(){
    return (this.loc.x < 0) || this.hasEscaped;
  }
  
  // is this fish a food for the given shark?
  boolean isFood(SharkImp shark){
    return
      Math.sqrt((this.loc.x - shark.loc.x) * (this.loc.x - shark.loc.x) +
                (this.loc.y - shark.loc.y) * (this.loc.y - shark.loc.y)) < 25;     
  }

  // draw this fish on the given Canvas
  void draw(Canvas c){
    c.drawDisk(this.loc, 10, new Red());
    c.drawDisk(new Posn(this.loc.x - 8, this.loc.y), 4, new Red());
    c.drawDisk(new Posn(this.loc.x - 6, this.loc.y), 1, new Yellow());
    c.drawRect(new Posn(this.loc.x + 9, this.loc.y - 4), 4, 8, new Red());
  }
  
  // produce a new fish at the right quarter of the canvas at random height
  FishImp makeFish(int WIDTH, int HEIGHT){
    return new FishImp(new Posn(180, this.loc.y), false, this.speed);
  }
    
  // produce a random initial coordinate of the fish
  int randomLimited(int LIMIT){
    return Math.abs(new Random().nextInt()) % LIMIT;
  }
  
  // replace this fish with a new one
  void feedShark(SharkImp shark){
    this.loc = new Posn(180, this.loc.y);
    this.hasEscaped = false;
  }
}

/*
+----------------+
| OceanWorld     |
+----------------+
| SharkImp shark |
| FishImp fish   |
+----------------+

*/


/*
          +------------+                
          | ILoFishImp |<--------------+
          +------------+               |
          +------------+               |
                 |                     |
                / \                    |
                ---                    |
                 |                     |
       -------------------             |
       |                 |             |
+-------------+ +-----------------+    |
| MtLoFishImp | | ConsLoFishImp   |    |
+-------------+ +-----------------+    |
+-------------+ | FishImp first   |    |
                | ILoFishImp rest |----+
                +-----------------+ 
*/

// to represent a school of fish
interface ILoFishImp {
 
   // produce from this fish the one that swam for a minute more
  void onTick();

  // is there a fish in this school that can be eaten by the given shark?
  boolean isFood(SharkImp shark);

  // produce a school of fish with one eaten and replaced by a new random one
  void feedShark(SharkImp shark);
  
  // draw this fish on the given Canvas
  void draw(Canvas c);
  
}

// to represent an empty school of fish
class MtLoFishImp implements ILoFishImp {

  MtLoFishImp() {
  }

   // produce from this fish the one that swam for a minute more
  public void onTick(){ }

  // is there a fish in this school that can be eaten by the given shark?
  public boolean isFood(SharkImp shark){
    return false;
  }

  // produce a school of fish with one eaten and replaced by a new random one
  public void feedShark(SharkImp shark){ }

  // draw this fish on the given Canvas
  public void draw(Canvas c){}
}

// to represent a nonempty school of fish
class ConsLoFishImp implements ILoFishImp {
  FishImp first;
  ILoFishImp rest;

  ConsLoFishImp(FishImp first, ILoFishImp rest) {
    this.first = first;
    this.rest = rest;
  }

   // produce from this school of fish the one where all swam for a minute more
  public void onTick(){
    if (this.first.escaped())
      this.first = this.first.makeFish(200, 200);
    else
      this.first.onTick();
    this.rest.onTick();
  }
  
  // is there a fish in this school that can be eaten by the given shark?
  public boolean isFood(SharkImp shark){
    return this.first.isFood(shark) || this.rest.isFood(shark);
  }

  // produce a school of fish with one eaten and replaced by a new random one
  public void feedShark(SharkImp shark){
    if (this.first.isFood(shark))
      this.first.feedShark(shark);
    else 
      this.rest.feedShark(shark);
  }

  // draw this fish on the given Canvas
  public void draw(Canvas c){
    this.first.draw(c);
    this.rest.draw(c);
  }
}

/*
The world includes a school of fish. When a fish is eaten, a new fish appears
on the right hand side. The shark grows by a fixed amount, but the growth is
limited by a fixed upper bound. SharkImp's display reflects its growth.
Every fish is given a new random speed when it appears, it keeps swimming
at the same speed until either eaten or it escapes.
*/
// to represent an ocean world
class OceanWorldImperative extends World{
  SharkImp shark;
  ILoFishImp fish;
  int WIDTH =  200;
  int HEIGHT = 200;

  OceanWorldImperative(SharkImp shark, ILoFishImp fish) {
    this.shark = shark;
    this.fish = fish;
  }
  
  // start the world and the timer
  void go() { this.bigBang(200, 200, 0.6); }

  // produce a new OceanWorldImperative after one minute elapsed: 
  // move the fish, starve the shark, check if the fish is eaten or has escaped
  public void onTick(){
    // if the shark found fish, fed the shark, replace the fish with a new one                        
    if (this.fish.isFood(this.shark)){
      this.shark.getFatter();
      this.fish.feedShark(shark);
    }
    // if the shark starved to death, end the world
    else if(this.shark.isDead())
      this.endOfWorld("the shark starved to death");
    
    // no special events, just move the fish and starve the shark 
    else {
      this.shark.onTick();
      this.fish.onTick();
    }
  }
  
  // produce a new OceanWorldImperative in response to the given key press
  public void onKeyEvent(String ke){
    this.shark.onKeyEvent(ke);
  }
  
  // draw this world
  public void draw(){
    this.theCanvas.drawRect(new Posn(0, 0), 
                            this.WIDTH, this.HEIGHT, new Blue());
    this.fish.draw(this.theCanvas);
           this.shark.draw(this.theCanvas);
  }
 
}

// Examples of the classes FishImp, SharkImp, OceanWorldImperative and tests for all methods
class ExamplesImp{
  ExamplesImp(){}

  /** Examples of Sharks */
  SharkImp mac = new SharkImp(new Posn(10, 100), 200);
  SharkImp mac2 = new SharkImp(new Posn(10, 100), 100);
  SharkImp mac3 = new SharkImp(new Posn(10, 100), -1);

  /** Examples of Fish */
  FishImp fishy = new FishImp(new Posn(190, 100), false, 8);
  FishImp yummy = new FishImp(new Posn(15, 98), false, 8);
  FishImp gone = new FishImp(new Posn(15, 98), true, 5);
  FishImp gone2 = new FishImp(new Posn(-2, 98), false, 5);
  
  /** Examples of fish that will be in the list of Fish */
  FishImp fishy1 = new FishImp(new Posn(190, 160), false, 6);
  FishImp fishy2 = new FishImp(new Posn(190, 120), false, 5);
  FishImp fishy3 = new FishImp(new Posn(190, 140), false, 4);
  FishImp fishy4 = new FishImp(new Posn(190, 100), false, 8);
  FishImp fishy5 = new FishImp(new Posn(190, 180), false, 9);
  
  /** Method that prints the location of fish that comprise lists of fish */
  void printFishies(){
    System.out.println("fishy1: (" + fishy1.loc.x + ", " + fishy1.loc.y + ")");
    System.out.println("fishy2: (" + fishy2.loc.x + ", " + fishy1.loc.y + ")");
    System.out.println("fishy3: (" + fishy3.loc.x + ", " + fishy1.loc.y + ")");
    System.out.println("fishy4: (" + fishy4.loc.x + ", " + fishy1.loc.y + ")");
    System.out.println("fishy5: (" + fishy5.loc.x + ", " + fishy1.loc.y + ")");
  }
  
  /** Reset the values of the Shark test data */
  void resetSharks(){
    mac = new SharkImp(new Posn(10, 100), 200);
    mac2 = new SharkImp(new Posn(10, 100), 100);
    mac3 = new SharkImp(new Posn(10, 100), -1);  
  }
  
  /** Reset the values of the Fish test data */
  void resetFishies(){
    fishy = new FishImp(new Posn(190, 100), false, 8);
    yummy = new FishImp(new Posn(15, 98), false, 8);
    gone = new FishImp(new Posn(15, 98), true, 5);
    gone2 = new FishImp(new Posn(-2, 98), false, 5);
    
    fishy1 = new FishImp(new Posn(190, 160), false, 6);
    fishy2 = new FishImp(new Posn(190, 120), false, 5);
    fishy3 = new FishImp(new Posn(190, 140), false, 4);
    fishy4 = new FishImp(new Posn(190, 100), false, 8);
    fishy5 = new FishImp(new Posn(190, 180), false, 9); 
  }
  
  /** Examples of lists of Fish */
  ILoFishImp nofish = new MtLoFishImp();
  ILoFishImp fishies = new ConsLoFishImp(fishy1,
                       new ConsLoFishImp(fishy2,
                       new ConsLoFishImp(fishy3,
                       new ConsLoFishImp(fishy4,
                       new ConsLoFishImp(fishy5, new MtLoFishImp())))));
                    
  ILoFishImp manyfish = new ConsLoFishImp(fishy,
                        new ConsLoFishImp(yummy, new MtLoFishImp()));
                    
    
  /** Reset the values of the lists of Fish test data */
  void resetFishLists(){
    // remember to reset the fihe data first
    resetFishies();
    
    fishies = new ConsLoFishImp(fishy1,
              new ConsLoFishImp(fishy2,
              new ConsLoFishImp(fishy3,
              new ConsLoFishImp(fishy4,
              new ConsLoFishImp(fishy5, new MtLoFishImp())))));

    manyfish = new ConsLoFishImp(fishy,
               new ConsLoFishImp(yummy, new MtLoFishImp()));
  }
  
  /** A sample OceanWorld */
  OceanWorldImperative sfw = 
    new OceanWorldImperative(this.mac, this.fishies); 
  
  /**
   * Reset the whole ocean world
   */
  void resetOceanWorld(){
    // make sure your data is reset
    this.resetSharks();
    this.resetFishLists();
    
    sfw = new OceanWorldImperative(this.mac, this.fishies); 
  }
  
  Canvas c = new Canvas(200, 200);
  
  /**
   * Test the method onKeyEvent in the class SharkImp
   * @param t the <CODE>{@link Tester Tester}</CODE> that performs the tests
   */
  @TestMethod
  void testSharkOnKeyEvent(Tester t){
    // make sure your data is reset
    resetSharks();
    
    // shark ignores "left" key
    this.mac.onKeyEvent("left");
    t.checkExpect(this.mac.loc, new Posn(10, 100));

    // shark moves "up"
    this.mac.onKeyEvent("up");
    t.checkExpect(this.mac.loc, new Posn(10, 97));

    // shark moves "down" - back to the same location
    this.mac.onKeyEvent("down");
    t.checkExpect(this.mac.loc, new Posn(10, 100));
  }

  /**
   * A visual test for drawing a SharkImp
   */
  void drawShark(){
    c.show();
    this.mac.draw(c);
  }
  
  /** 
   *  Test the method onTick in the class SharkImp
   * @param t the <CODE>{@link Tester Tester}</CODE> that performs the tests
   */
  @TestMethod
  void testSharkOnTick(Tester t){
    // make sure your data is reset 
    this.resetSharks();
    
    // shark gets hungrier
    this.mac.onTick();
    t.checkExpect(this.mac.life, 199); 
    
    // dead shark stays dead
    SharkImp dead = new SharkImp(new Posn(10, 100), 0);
    dead.onTick();
    t.checkExpect(dead.life,0); 
  }  

  /**
   * Test the method getFatter in the class SharkImp
   * @param t the <CODE>{@link Tester Tester}</CODE> that performs the tests
   */
  @TestMethod
  void testGetFatter(Tester t){
    // make sure your data is correct
    this.resetSharks();
    
    // fatten the shark - but there is a limit of 220
    mac.getFatter();
    t.checkExpect(mac.life, 220);
    
    // fatten another one - this one eats all he can
    mac2.getFatter();
    t.checkExpect(mac2.life, 130);
  }
  
  /**
   * Test the method isDead in the class SharkImp
   * @param t the <CODE>{@link Tester Tester}</CODE> that performs the tests
   */
  @TestMethod
  void testIsDead(Tester t){
    // make sure your data is correct
    this.resetSharks();
    
    t.checkExpect(mac.isDead(), false);
    t.checkExpect(mac3.isDead(), true);
  }

  /**
   * Test the method onTick in the class FishImp
   * @param t the <CODE>{@link Tester Tester}</CODE> that performs the tests
   */
  @TestMethod
  void testFishOnTick(Tester t){
    // make sure your data is correct
    this.resetFishies(); 
    
    // just move if all is well
    this.fishy.onTick(); 
    t.checkExpect(this.fishy.loc, new Posn(182, 104));
    
    // escaped fish is replaced by a new one
    FishImp gonner = new FishImp(new Posn(-2, 100), true, 10);
    gonner.onTick();
    t.checkExpect(gonner.loc, new Posn(200, 200));
    t.checkExpect(gonner.hasEscaped, false);
    t.checkExpect(gonner.speed, 10);
          
    // fish got out of bounds - set status to 'has escaped'
    FishImp runner = new FishImp(new Posn(-2, 100), false, 8);
    runner.onTick();
    t.checkExpect(runner.hasEscaped, true); 
  }
  
 
  /**
   * Test the method escaped in the class FishImp
   * @param t the <CODE>{@link Tester Tester}</CODE> that performs the tests
   */
  @TestMethod
  void testFishEscaped(Tester t){
    // make sure your data is correct
    this.resetFishies(); 

    t.checkExpect(this.fishy.escaped(), false);
    t.checkExpect((new FishImp(new Posn(-2, 100), true, 8)).escaped(), true);
    t.checkExpect((new FishImp(new Posn(-2, 100), false, 8)).escaped(), true); 
  }

  /**
   * Test the method isFood in the class FishImp
   * @param t the <CODE>{@link Tester Tester}</CODE> that performs the tests
   */
  @TestMethod
  void testFishIsFood(Tester t){
    // make sure your data is correct
    this.resetFishies(); 
    
    t.checkExpect(this.fishy.isFood(this.mac), false);
    t.checkExpect(this.yummy.isFood(new SharkImp(new Posn(10, 100), 20)), true);
  } 

  /**
   * A visual test for drawing a fish
   */
  void drawFishImp(){
    c.show();
    this.fishy.draw(c);
  }
  
  
  /**
   * Test the method randomLimited in the class FishImp
   * @param t the <CODE>{@link Tester Tester}</CODE> that performs the tests
   */
  @TestMethod
  void testRandomLimited(Tester t){
    t.checkExpect(this.fishy.randomLimited(5) < 6 && 
                  this.fishy.randomLimited(5) >= 0,
                  true);
  }

  /**
   * Test the method makeFish in the class FishImp
   * @param t the <CODE>{@link Tester Tester}</CODE> that performs the tests
   */
  @TestMethod
  void testMakeFish(Tester t){
    t.checkExpect(this.fishy.makeFish(200, 200).hasEscaped, false);
    t.checkExpect(this.fishy.makeFish(200, 200).loc.y < 200, true);
    t.checkExpect(this.fishy.makeFish(200, 200).loc.y >=  0, true);
    t.checkExpect(this.fishy.makeFish(200, 200).loc.x < 200, true);
    t.checkExpect(this.fishy.makeFish(200, 200).loc.x >= 150, true);
    t.checkExpect(this.fishy.makeFish(200, 200).speed <  10, true);
    t.checkExpect(this.fishy.makeFish(200, 200).speed >   1, true);
  }
 
  /**
   * Test the method onTick in the classes that represent a school of fish
   * @param t the <CODE>{@link Tester Tester}</CODE> that performs the tests
   */
  @TestMethod
  void testILoFishOnTick(Tester t){
    // make sure your data is correct
    this.resetFishLists();
    
    FishImp fishyTemp = new FishImp(new Posn(190, 100), false, 8);
    FishImp yummyTemp = new FishImp(new Posn(15, 98), false, 8);

    this.manyfish.onTick();   
    fishyTemp.onTick();
    yummyTemp.onTick();;
    
    t.checkExpect(this.fishy, fishyTemp);
    t.checkExpect(this.yummy, yummyTemp);
  }

  /**
   * Test the method isFood in the classes that represent a school of fish
   * @param t the <CODE>{@link Tester Tester}</CODE> that performs the tests
   */
  @TestMethod
  void testILoFishIsFood(Tester t){
    // make sure your data is correct
    this.resetFishLists();
    
    t.checkExpect(this.nofish.isFood(this.mac), false);
    t.checkExpect(this.manyfish.isFood(this.mac), true);
    t.checkExpect(this.fishies.isFood(this.mac), false);
  }

  /**
   * Test the method feedShark in the classes that represent a school of fish
   * @param t the <CODE>{@link Tester Tester}</CODE> that performs the tests
   */
  @TestMethod
  void testILoFishFeedShark(Tester t){
    // make sure your data is correct
    this.resetFishLists();

    FishImp fishyTemp = new FishImp(new Posn(190, 100), false, 8);
    FishImp yummyTemp = new FishImp(new Posn(15, 98), false, 8);
    yummyTemp.feedShark(this.mac);
    
    this.manyfish.feedShark(this.mac);  
    FishImp first = ((ConsLoFishImp)this.manyfish).first;
    FishImp eaten = ((ConsLoFishImp)((ConsLoFishImp)this.manyfish).rest).first;
 
    t.checkExpect(first, fishyTemp);
    t.checkExpect(eaten, yummyTemp);
  }

  /**
   * A visual test for drawing a school of fish
   */
  void drawFishies(){
    c.show();
    this.manyfish.draw(c);
  }

  /**
   * Test the method onKeyEvent in the class OceanWorldImperative
   * @param t the <CODE>{@link Tester Tester}</CODE> that performs the tests
   */
  @TestMethod
  void testOceanOnKeyEvent(Tester t){
    // make sure your data is reset
    this.resetOceanWorld();
    
    SharkImp macTemp = new SharkImp(new Posn(10, 100), 200);
    SharkImp macTempUp = new SharkImp(new Posn(10, 97), 200);

    // nothing happens on "left" key event
    this.sfw.onKeyEvent("left");    
    t.checkExpect(this.sfw, new OceanWorldImperative(macTemp, this.sfw.fish));
    
    // on "up" shark moves up - fish stay put
    this.sfw.onKeyEvent("up");       
    t.checkExpect(this.sfw, new OceanWorldImperative(macTempUp, this.sfw.fish));
 
    // on "down" shark moves back down - fish stay put
    this.sfw.onKeyEvent("down");       
    t.checkExpect(this.sfw, new OceanWorldImperative(macTemp, this.sfw.fish));
  }
 
  /** Exmples od the ocean world - to be used in the final tests */
  OceanWorldImperative sfwEnd = new OceanWorldImperative(this.mac3, this.fishies);
  OceanWorldImperative sfwReg = new OceanWorldImperative(this.mac, this.fishies);
  OceanWorldImperative sfwYum = new OceanWorldImperative(this.mac, this.manyfish);
  
  /**
   * Test the method onKeyEvent in the class OceanWorldImperative
   * @param t the <CODE>{@link Tester Tester}</CODE> that performs the tests
   */
  @TestMethod
  void testOceanOnTick(Tester t){

      // sfwReg.go();
      
      // test that shark gets eaten and the fish restocked
      
      // test that fish on the escape edge is marked
      
      // test that escaped fish is replaced by a new one
      
      // test the the world ends when the shark dies
      
      // check that the fish move as expected
      
  } 
  
  /**
   * a visual test for drawing the OceanWorldImperative - cannot be run
   */
  void drawOcean(){
    this.sfw.draw();
  }

  
  // test the method onTick in the class OceanWorldImperative
  void testOceanWorldGo(Tester t){
    sfwReg.go();
  }
}
